﻿//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: ObjectHOlderBase.cs
//
//  Description: Base class for all object holder classes.
// 
//--------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;
using System.Xml.Serialization;
using System.Web;
using System.Web.SessionState;
using Chili.Opf3.Query;
using Chili.Opf3.Query.OPathParser;
using System.Security.Permissions;
using Chili.Opf3.Mapping;
using System.Collections;

namespace Chili.Opf3.Relations
{
	/// <summary>
	/// Abstract base class for ObjectHolder classes. Contains the base functionality that is required in any ObjectHolder class. 
	/// </summary>
	[Serializable]
	public abstract class ObjectHolderBase : ISerializable
	{
		private ObjectContext _context;
		private RelationAttribute _attribute;
		private object _parent;
		private IQuery _query;
		private bool _isLoaded;
		private bool _automaticLoad = true;
		private const string VIEWSTATE_ID = "{DB21F18A-4555-4af7-8D1C-CFA1791BA655}";
		private object _object;
		private bool _useRemotingSerialization;

		/// <summary>
		/// Creates a new instance of the <see cref="ObjectHolderBase">ObjectHolderBase Class</see>.
		/// </summary>
		protected internal ObjectHolderBase()
		{
		}

		/// <summary>
		/// Creates a new instance of the <see cref="ObjectHolderBase">ObjectHolderBase Class</see>.
		/// </summary>
		/// <param name="query"><see cref="Opf3.Query.IQuery">IQuery</see> that is executed to load the associated 
		/// objects. This parameter allows you to use any kind of query without using the build in functionality.</param>
		protected internal ObjectHolderBase(IQuery query)
		{
			this.Query = query;
		}

		/// <summary>
		/// Gets the <see cref="ObjectContext" /> associated with this instance. Might be null if
		/// no storage operation has been performed on the parent or related object(s).
		/// </summary>
		/// <remarks>
		/// This is the default <see cref="ObjectContext" /> that's used to load and save the related 
		/// objects and the same that has been used to load or save the parent object. It might be 
		/// null if there has been no storage operation (load or persist changes) performed on the 
		/// parent or related object(s).
		/// </remarks>
		[XmlIgnore]
		public ObjectContext ObjectContext
		{
			get { return _context; }
			protected internal set { _context = value; }
		}

		/// <summary>
		/// Specifies the <see cref="Opf3.Relations.RelationAttribute">RelationAttribute</see> 
		/// associated with the ObjectHolder. The attribute specifies the relation between the
		/// related objects and the parent object.
		/// </summary>
		/// <value>The <see cref="Opf3.Relations.RelationAttribute">RelationAttribute</see> associated 
		/// with the ObjectHolder or null.</value>
		[XmlIgnore]
		protected internal RelationAttribute RelationAttribute
		{
			get { return _attribute; }
			set { _attribute = value; }
		}

		/// <summary>
		/// Gets or sets whether the related objects should be loaded in automatic when fetching the
		/// InnerObject property.
		/// </summary>
		[XmlIgnore]
		protected internal bool AutomaticLoad
		{
			get { return _automaticLoad; }
		}

		/// <summary>
		/// Specifies the parent persistent object of the ObjectHolder.
		/// </summary>
		/// <value>The parent persistent object associated with the ObjectHolder or null.</value>
		[XmlIgnore]
		protected internal object Parent
		{
			get { return _parent; }
			set { _parent = value; }
		}

		/// <summary>
		/// Gets or sets the <see cref="Opf3.Query.IQuery">IQuery</see> associated with the ObjectHolder.
		/// </summary>
		/// <value>The <see cref="Opf3.Query.IQuery">IQuery</see> associated with the ObjectHolder or null.</value>
		public IQuery Query
		{
			get { return _query; }
			set { _query = value; }
		}

		/// <summary>
		/// Gets and sets the object that is managed by this instance.
		/// </summary>
		protected internal object ManagedObject
		{
			get { return _object; }
			set { _object = value; }
		}

		/// <summary>
		/// Returns whether the related objects have been loaded.
		/// </summary>
		/// <value>True if the related persistent objects have been loaded.</value>
		public bool IsLoaded
		{
			get { return _isLoaded; }
			internal set { _isLoaded = value; }
		}

		/// <summary>
		/// Gets whether the relation is attached to the parent object. This means that the relation has all
		/// required information to be loaded from the storage.
		/// </summary>
		[XmlIgnoreAttribute]
		public bool IsAttached
		{
			get { return (_context != null && _attribute != null && _parent != null); }
		}

		/// <summary>
		/// Sets the <see cref="Opf3.ObjectContext">ObjectContext</see> associated with the ObjectHolder, 
		/// the <see cref="Opf3.Relations.RelationAttribute">RelationAttribute</see> containing the information
		/// about the relation between the object and it's related objects and the parent persistent object. 
		/// It's required to gain access to properties of the parent object. Those properties are used to 
		/// compile the query to load the related objects.
		/// </summary>
		/// <param name="context"><see cref="Opf3.ObjectContext">ObjectContext</see> that is used 
		/// by the ObjectHolder to load and persist the objects.</param>
		/// <param name="attribute">The <see cref="Opf3.Relations.RelationAttribute">RelationAttribute</see> 
		/// that is set.</param>
		/// <param name="parent">Object that is set as parent.</param>
		/// <param name="automaticLoad">True to load related objects when querying the InnerObject property.</param>
		/// <param name="useRemotingSerialization">True if the serialization for remoting should get used.</param>
		internal void SetObjectHolderData(ObjectContext context, RelationAttribute attribute, object parent, 
			bool automaticLoad, bool useRemotingSerialization)
		{
			_context = context;
			_attribute = attribute;
			_parent = parent;
			_automaticLoad = automaticLoad;
			_useRemotingSerialization = useRemotingSerialization;
		}

		/// <summary>
		/// Invalidates the current instance. The next request of the InnerObject property reloads all the 
		/// related objects. This is only true if the automatic load of the related objects has been enabled. 
		/// Otherwise the reload method needs to be invoked (after calling Invalidate) to do a manual reload.
		/// </summary>
		public virtual void Invalidate()
		{
			_isLoaded = false;
		}

		/// <summary>
		/// Loads the related objects from the storage.
		/// </summary>
		/// <remarks>This method takes the instance of the <see cref="ObjectContext" />
		/// that's currently associated with the holder class. This usually is the same instance
		/// that has been used to load the parent object.</remarks>
		public void Load()
		{
			Load(true);
		}

		/// <summary>
		/// Loads the related object from the storage by using the given
		/// instance of the <see cref="ObjectContext" /> Class.
		/// </summary>
		/// <param name="context">The <see cref="ObjectContext"/> used to load the related objects.</param>
		public void Load(ObjectContext context)
		{
			Load(context, true);
		}

		/// <summary>
		/// Loads the related objects from the storage.
		/// </summary>
		/// <remarks>This method takes the instance of the <see cref="ObjectContext" />
		/// that's currently associated with the holder class. This usually is the same instance
		/// that has been used to load the parent object.</remarks>
		/// <param name="throwOnNotAttached">True to throw an exception if the related object or 
		/// objects can't be successfully loaded into the local collection or reference because the required data
		/// (such as an ObjectContext) hasn't been set yet.</param>
		public void Load(bool throwOnNotAttached)
		{
			if (IsLoaded)
				return;

			if (throwOnNotAttached)
				ValidateLoad(true);

			InnerLoad(this.ObjectContext);

			Tracing.Current.TraceStorageIO("Loaded persistent's related objects from storage.");
			// Set the ObjectSetHolder as loaded.
			IsLoaded = true;
		}

		/// <summary>
		/// Loads the related object from the storage by using the given
		/// instance of the <see cref="ObjectContext" /> Class.
		/// </summary>
		/// <param name="context">The <see cref="ObjectContext"/> used to load the related objects.</param>
		/// <param name="throwOnNotAttached">True to throw an exception if the related object or 
		/// objects can't be successfully loaded into the local collection or reference because the required data
		/// (such as an ObjectContext) hasn't been set yet.</param>
		public void Load(ObjectContext context, bool throwOnNotAttached)
		{
			if (context == null)
				throw new ArgumentNullException("context");

			if (throwOnNotAttached)
				ValidateLoad(true);

			// set the specified ObjectContext.
			InnerLoad(context);

			Tracing.Current.TraceStorageIO("Loaded persistent's related objects from storage.");
			// Set the ObjectSetHolder as loaded.
			IsLoaded = true;
		}

		/// <summary>
		/// Checks if the load can be performed because the required data is set.
		/// </summary>
		private void ValidateLoad(bool checkForContext)
		{
			if (checkForContext && _context == null)
				throw new InvalidOperationException("Can't load because no ObjectContext is associated with the relation.");
			if (_attribute == null)
				throw new InvalidOperationException("Can't load because the relation is not attached to its parent.");
		}

		#region Abstract methods

		/// <summary>
		/// This method is invoked to persist the changes on the objects in the ObjectHolder.
		/// </summary>
		/// <param name="context"><see cref="Opf3.ObjectContext">ObjectContext</see> used to persist 
		/// the changes. It is the same <see cref="Opf3.ObjectContext">ObjectContext</see> as used by the
		/// parent object.</param>
		/// <param name="persistDepth">Specifies the depth of the loaded object graph that is persisted.</param>
		internal abstract void PersistChanges(ObjectContext context, PersistDepths persistDepth);

		/// <summary>
		/// This method connects the foreign-key properties in the child persistent objects with the identifier
		/// properties of the parent object.
		/// </summary>
		internal abstract void ConnectPropertiesInChildObjects();

		/// <summary>
		/// This method is invoked to check if any of the ObjectHolder's objects have been modified.
		/// </summary>
		/// <param name="context"><see cref="Opf3.ObjectContext">ObjectContext</see> used to check for modified 
		/// objects. It is the same <see cref="Opf3.ObjectContext">ObjectContext</see> as used by the
		/// parent object.</param>
		/// <returns>True if an object has been modified; false othewise.</returns>
		internal abstract bool HasModifiedObjects(ObjectContext context);

		/// <summary>
		/// Loads the related object(s) from the storage.
		/// </summary>
		protected abstract void InnerLoad(ObjectContext context);

		/// <summary>
		/// This method is invoked to check if the object holder has removed objects.
		/// </summary>
		/// <returns>True if an object has been removed; false otherwise</returns>
		internal abstract bool HasRemovedObjects();

		/// <summary>
		/// Gets the enumerator for the given loaded related data. This method doesn't load
		/// data from the storage but rather returns the currently loaded data.
		/// </summary>
		internal abstract IEnumerable GetRawEnumerator();

		#endregion

		#region UpdateConnectedProperty

		/// <summary>
		/// Updates the connected property. This is done after a save to the storage.
		/// </summary>
		/// <param name="obj">Objects that's property is updated.</param>
		protected internal void UpdateConnectedProperty(object obj)
		{
			// If the object is null return immediately.
			if (obj == null)
				return;

			// We can savely return if there is a weak persistent specified. It is a many-to-many relation
			// where no update has to be done on the properties.
			if (_attribute.WeakPersistent != null)
				return;

			PersistRelationships persistRelationShip = _attribute.PersistRelationship;

			TypeMapping parentMapping = TypeMapping.GetTypeMapping(_parent);
//			TypeMapping childMapping = TypeMapping.GetTypeMapping(obj);

			// Get the RelationInfo object that represents the relation.
			IRelationInfo info = parentMapping.Relations.GetRelationMapping(_attribute.Name);

			foreach (RelatedPropertiesPair pair in info.RelatedProperties)
			{
				if (persistRelationShip == PersistRelationships.ParentFirst)
				{
					// Get the value from the parent object.
					object value = pair.ParentMember.GetValue(_parent);
					// Set the child property with the value of the parent property.
					pair.ChildMember.SetValue(obj, value);
				}
				else
				{
					// Get the value from the child object.
					object value = pair.ChildMember.GetValue(obj);
					// Set the parent with the value of the child object.
					pair.ParentMember.SetValue(_parent, value);
				}
			}
		}

		#endregion

		#region ISerializable Members

		/// <summary>
		/// Creates a new instance of the <see cref="ObjectHolderBase">ObjectHolderBase Class</see>.
		/// </summary>
		/// <param name="info"></param>
		/// <param name="context"></param>
		protected ObjectHolderBase(SerializationInfo info, StreamingContext context)
		{
			string id = null;

			HttpSessionState session = GetHttpSession();
			// restore the unique identifier.
			if (session != null)
				id = (string)info.GetValue(VIEWSTATE_ID, typeof(string));

			_useRemotingSerialization = GetValueFromSerialization<bool>(info, session, id, "UseRemotingSerialization");

			// some values are only deserialized if the remoting serialization is enabled.
			if (_useRemotingSerialization)
			{
				_context = GetValueFromSerialization<ObjectContext>(info, session, id, "ObjectContext");
				_attribute = GetValueFromSerialization<RelationAttribute>(info, session, id, "RelationAttribute");
				_parent = GetValueFromSerialization<object>(info, session, id, "Parent");
			}

			_automaticLoad = GetValueFromSerialization<bool>(info, session, id, "AutomaticLoad");
			_query = GetValueFromSerialization<IQuery>(info, session, id, "Query");
			_object = GetValueFromSerialization<object>(info, session, id, "ManagedObject");
			_isLoaded = GetValueFromSerialization<bool>(info, session, id, "IsLoaded");
		}

		/// <summary>
		/// Create a backup of the object in the session. We do this to not blow
		/// up the ViewState in ASP.NET applications. When serializing the object in the
		/// ViewState we would also send the <see cref="Opf3.ObjectContext">ObjectContext</see>
		/// and other classes such as the storage to the ViewState.
		/// </summary>
		/// <param name="info"></param>
		/// <param name="context"></param>
		[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
		public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
		{
			// create an unique identifier.
			string id = null;
			HttpSessionState session = GetHttpSession();

			if (session != null)
			{
				id = Guid.NewGuid().ToString(); 
				info.AddValue(VIEWSTATE_ID, id);
			}
			
			// some values are only serialized if the remoting serialization is enabled.
			if (_useRemotingSerialization)
			{
				AddValueToSerialization(info, session, id, "ObjectContext", _context);
				AddValueToSerialization(info, session, id, "RelationAttribute", _attribute);
				AddValueToSerialization(info, session, id, "Parent", _parent);
			}

			AddValueToSerialization(info, session, id, "AutomaticLoad", _automaticLoad);
			AddValueToSerialization(info, session, id, "Query", _query);
			AddValueToSerialization(info, session, id, "ManagedObject", _object);
			AddValueToSerialization(info, session, id, "IsLoaded", _isLoaded);
			AddValueToSerialization(info, session, id, "UseRemotingSerialization", _useRemotingSerialization);
		}

		/// <summary>
		/// Gets and returns the value from the serialization.
		/// </summary>
		private T GetValueFromSerialization<T>(SerializationInfo info, HttpSessionState session, string id, string name)
		{
			if (session != null)
			{
				// get the value and remove it from the session.
				string n = id + name;
				object value = session[n];
				session.Remove(n);

				return (T)value;
			}
			
			return (T)info.GetValue(name, typeof(T));
		}

		/// <summary>
		/// Adds a given name value pair to the serialization.
		/// </summary>
		private void AddValueToSerialization(SerializationInfo info, HttpSessionState session, string id, string name, object value)
		{
			if (session == null)
				info.AddValue(name, value);
			else
				session.Add(id + name, value);
		}

		/// <summary>
		/// Check if a HttpSession is available. If not we have to serialize the whole object.
		/// </summary>
		private HttpSessionState GetHttpSession()
		{
			// Get the HttpContext.
			HttpContext context = HttpContext.Current;
			if (context == null)
				return null;
			// Check if we got a session. We are going to store the ObjectContext into the session.
			return context.Session;
		}

		#endregion
	}
}
